package station;

/**
 * Vorlesung: Verteilte Systeme
 * Aufgabe  : 04
 * 
 * @author Benjamin Burchard
 * @author Tell Mueller-Pettenpohl
 * 
 */

import static station.DataMessage.DATA_LENGTH;
import static timeDivisionMultipleAccess.Channel.FRAME_DURATION;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import timeDivisionMultipleAccess.Channel;
import timeDivisionMultipleAccess.TimeSyncer;
import timeDivisionMultipleAccess.exceptions.FrameIsFullException;
import timeDivisionMultipleAccess.exceptions.FrameTimeoutException;

public class Station {

	private ScheduledThreadPoolExecutor schExecuter;
	private byte buffer[];
	private DatagramSocket udpSocket;
	private String mcastAddr;
	private int mcastPort;
	private String ifaceAddr;
	private boolean isRunning = true;
	private long lastSlotTime;
	private byte nextFreeSlot;
	private long nextSlotDelay;
	private Channel channel;
	private TimeSyncer syncer;

	public Station(String ifaceAddr, int udpPort, String mcastAddr,
			int mcastPort) {
		this.schExecuter = new ScheduledThreadPoolExecutor(20);
		this.buffer = new byte[DATA_LENGTH];
		try {
			this.udpSocket = new DatagramSocket(udpPort);
		} catch (SocketException e) {
			e.printStackTrace();
		}
		this.mcastAddr = mcastAddr;
		this.mcastPort = mcastPort;
		this.ifaceAddr = ifaceAddr;
		this.isRunning = true;
		this.nextFreeSlot = Byte.MIN_VALUE;
		this.nextSlotDelay = 0;
		this.lastSlotTime = 0;
		this.channel = new Channel();
		this.syncer = new TimeSyncer();
	}

	private void listenOnChannel() {
		try {
			Thread.sleep(1000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	// TODO only search a new slot if there is a new send
	private void scheduleSendMsg() throws FrameTimeoutException {
		//TODO To fix that every station only sends one time per frame
		// - stop receive and force it
		// - calculate with the receive time a schedule in the further future
		checkForSkippedFrame();
		channel.shiftToNextFrame();
		if (nextFreeSlot == Byte.MIN_VALUE) {
			channel.unlockAllSlots();
			System.out.println("Listen on the channel!");
			listenOnChannel();
			if (!(channel.isFrameEmpty())) {
				lookupFreeSlot();
			}
		}
		lastSlotTime = nextSlotDelay + syncer.getCurrentTime();
		scheduleSlot(nextSlotDelay, nextFreeSlot);
	}

	private void checkForSkippedFrame() {
		if((syncer.getCurrentTime() - lastSlotTime) >= FRAME_DURATION) {
			nextFreeSlot = Byte.MIN_VALUE;
		}
	}

	private void lookupFreeSlot() throws FrameTimeoutException {
		try {
			nextFreeSlot = channel.searchNextFreeSlot();
			//TODO should be done by every schedule!
			nextSlotDelay = channel.calcNextSlotDelay(syncer.getCurrentTime(),
					nextFreeSlot);
		} catch (FrameIsFullException e) {
			e.printStackTrace();
		}
		if (nextSlotDelay == 0) {
			throw new FrameTimeoutException("This message is not scheduable.");
		}
	}

	public void receiveData() throws FrameTimeoutException {
		while (isRunning) {
			DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
			try {
				udpSocket.receive(packet);
				System.out.println("Station received packet: " + packet);
			} catch (IOException e) {
				e.printStackTrace();
			}
			scheduleSendMsg();
		}
		this.udpSocket.close();
	}

	public void scheduleSlot(long delay, byte nextFreeSlot) {
		System.out.println("Station scheduleSlot with delay: " + delay
				+ " and next slot is: " + nextFreeSlot);
		schExecuter.schedule(new Sender(this.mcastAddr, this.mcastPort,
				nextFreeSlot, this.buffer, this.syncer), delay,
				TimeUnit.MILLISECONDS);
	}

	public void startReceiver(int udpPortSrc, int udpPortSink)
			throws IOException {
		Receiver receiver = new Receiver(this.ifaceAddr, this.mcastAddr,
				this.mcastPort, udpPortSink, this.syncer, this.channel);
		receiver.start();
	}
}
